'use strict';

const Buffer = require('buffer').Buffer;
var $ = global.bitcore.util.preconditions;

// Cipher Block Chaining
// http://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Cipher-block_chaining_.28CBC.29
var CBC = function CBC(blockcipher, cipherkeybuf, ivbuf) {
    if (!(this instanceof CBC)) {
        return new CBC(blockcipher, cipherkeybuf, ivbuf);
    }

    this.blockcipher = blockcipher;
    this.cipherkeybuf = cipherkeybuf;
    this.ivbuf = ivbuf;
};

CBC.buf2blockbufs = function (buf, blocksize) {
    var bytesize = blocksize / 8;
    var blockbufs = [];

    for (var i = 0; i <= buf.length / bytesize; i++) {
        var blockbuf = buf.slice(i * bytesize, i * bytesize + bytesize);

        if (blockbuf.length < blocksize) {
            blockbuf = CBC.pkcs7pad(blockbuf, blocksize);
        }

        blockbufs.push(blockbuf);
    }

    return blockbufs;
};

CBC.blockbufs2buf = function (blockbufs) {
    var last = blockbufs[blockbufs.length - 1];
    last = CBC.pkcs7unpad(last);
    blockbufs[blockbufs.length - 1] = last;

    return Buffer.concat(blockbufs);
};

CBC.encrypt = function (messagebuf, ivbuf, blockcipher, cipherkeybuf) {
    var blocksize = ivbuf.length * 8;
    var blockbufs = CBC.buf2blockbufs(messagebuf, blocksize);
    var encbufs = CBC.encryptblocks(blockbufs, ivbuf, blockcipher, cipherkeybuf);
    return Buffer.concat(encbufs);
};

CBC.decrypt = function (encbuf, ivbuf, blockcipher, cipherkeybuf) {
    var blocksize = ivbuf.length * 8;
    var bytesize = ivbuf.length;
    var encbufs = [];
    for (var i = 0; i < encbuf.length / bytesize; i++) {
        encbufs.push(encbuf.slice(i * bytesize, i * bytesize + bytesize));
    }
    var blockbufs = CBC.decryptblocks(encbufs, ivbuf, blockcipher, cipherkeybuf);
    return CBC.blockbufs2buf(blockbufs, blocksize);
};

CBC.encryptblock = function (blockbuf, ivbuf, blockcipher, cipherkeybuf) {
    var xorbuf = CBC.xorbufs(blockbuf, ivbuf);
    return blockcipher.encrypt(xorbuf, cipherkeybuf);
};

CBC.decryptblock = function (encbuf, ivbuf, blockcipher, cipherkeybuf) {
    var xorbuf = blockcipher.decrypt(encbuf, cipherkeybuf);
    return CBC.xorbufs(xorbuf, ivbuf);
};

CBC.encryptblocks = function (blockbufs, ivbuf, blockcipher, cipherkeybuf) {
    var encbufs = [];

    for (const blockbuf of blockbufs) {
        var encbuf = CBC.encryptblock(blockbuf, ivbuf, blockcipher, cipherkeybuf);
        encbufs.push(encbuf);
        ivbuf = encbuf;
    }
    return encbufs;
};

CBC.decryptblocks = function (encbufs, ivbuf, blockcipher, cipherkeybuf) {
    var blockbufs = [];
    for (const encbuf of encbufs) {
        var blockbuf = CBC.decryptblock(encbuf, ivbuf, blockcipher, cipherkeybuf);
        blockbufs.push(blockbuf);
        ivbuf = encbuf;
    }

    return blockbufs;
};

CBC.pkcs7pad = function (buf, blocksize) {
    var bytesize = blocksize / 8;
    var padbytesize = bytesize - buf.length;
    var pad = new Buffer(padbytesize);
    pad.fill(padbytesize);
    return Buffer.concat([buf, pad]);
};

CBC.pkcs7unpad = function (paddedbuf) {
    var padlength = paddedbuf[paddedbuf.length - 1];
    var padbuf = paddedbuf.slice(paddedbuf.length - padlength, paddedbuf.length);
    var padbuf2 = new Buffer(padlength);
    padbuf2.fill(padlength);
    if (padbuf.toString('hex') !== padbuf2.toString('hex')) {
        throw new Error(padbuf.toString());
    }
    return paddedbuf.slice(0, paddedbuf.length - padlength);
};

CBC.xorbufs = function (buf1, buf2) {
    $.checkArgument(buf1.length === buf2.length, 'bufs must have the same length');
    var buf = new Buffer(buf1.length);
    for (var i = 0; i < buf1.length; i++) {
        buf[i] = buf1[i] ^ buf2[i];
    }
    return buf;
};

module.exports = CBC;
